package jxtend.processor.reflect;

import java.lang.reflect.Field;

import jxtend.processor.exception.JxtendReflectionException;

/**
 * Provides type-safe functionality for getting and setting the value of a (inaccessible) field through reflection.
 */
public final class ReflectField<C, F> {

	public final Field field;

	/**
	 * Creates a {@link ReflectField} instance for a 'accessible' (i.e. public) field.
	 *
	 * @param <C> the type that declares the field
	 * @param <F> the type of the field
	 * @param clazz the {@code Class} declaring the field
	 * @param fieldType the type of the field
	 * @param fieldName the name of the field
	 * @return a {@link ReflectField} instance
	 * @see #inaccessibleFieldOf(Class, String, Class)
	 */
	public static <C, F> ReflectField<C, F> getField(Class<C> clazz, Class<F> fieldType, String fieldName) {
		final Field field = getField(clazz, fieldName);
		return new ReflectField<C, F>(field, fieldType);
	}

	public static <C, F> ReflectField<C, F> findField(Class<C> clazz, Class<F> fieldType, String fieldName) {
		final Field field = findField(clazz, fieldName);
		if (field == null) {
			return null;
		}

		return new ReflectField<C, F>(field, fieldType);
	}

	public static <C, F> ReflectField<C, F> forField(Class<C> clazz, Class<F> fieldType, Field field) {
		if (!fieldType.equals(field.getType())) {
			throw new IllegalArgumentException(
					"Provided field type doesn't match the actual type of the field: " + field.getType().getName());
		}

		return new ReflectField<C, F>(field, fieldType);
	}
	
	public static <C, F> ReflectField<C, F> getPublicField(Class<C> clazz, Class<F> fieldType, String fieldName) {
		final Field field = getPublicField(clazz, fieldName);
		return new ReflectField<C, F>(field, fieldType);
	}

	public static <C, F> ReflectField<C, F> findPublicField(Class<C> clazz, Class<F> fieldType, String fieldName) {
		final Field field = findPublicField(clazz, fieldName);
		if (field == null) {
			return null;
		}

		return new ReflectField<C, F>(field, fieldType);
	}

	
	/**
	 * Returns the value of the field.
	 * @param <O> the type that declares the field or a subclass
	 * @param obj instance of which to return its field value 
	 * @return the value of the field
	 */
	public F get(C obj) {
		try {
			@SuppressWarnings("unchecked") F result = (F) field.get(obj);
			return result;
		} catch (IllegalArgumentException e) {
			throw e;
		} catch (IllegalAccessException e) {
			throw new JxtendReflectionException(e);
		}
	}

	/**
	 * Assigns a new value to the field.
	 * @param <O> the type that declares the field or a subclass
	 * @param <V> the type of the field or a subclass
	 * @param obj instance of which to assign a new value for the field 
	 * @param value the new value for the field
	 */
	public void set(C obj, F value) {
		try {
			field.set(obj, value);
		} catch (IllegalArgumentException e) {
			throw e;
		} catch (IllegalAccessException e) {
			throw new JxtendReflectionException(e);
		}
	}	

	private static <C> Field getField(Class<C> clazz, String fieldName) {
		try {
			return clazz.getDeclaredField(fieldName);
		} catch (NoSuchFieldException e) {
			throw new JxtendReflectionException(e);
		}
	}

	private static <C> Field findField(Class<C> clazz, String fieldName) {
		try {
			return clazz.getDeclaredField(fieldName);
		} catch (NoSuchFieldException e) {
			return null;
		}
	}
	
	private static <C> Field getPublicField(Class<C> clazz, String fieldName) {
		try {
			return clazz.getField(fieldName);
		} catch (NoSuchFieldException e) {
			throw new JxtendReflectionException(e);
		}
	}

	private static <C> Field findPublicField(Class<C> clazz, String fieldName) {
		try {
			return clazz.getField(fieldName);
		} catch (NoSuchFieldException e) {
			return null;
		}
	}

	private ReflectField(Field field, Class<F> fieldType) {
		try {
			this.field = field;
			
			if (!fieldType.isAssignableFrom(field.getType())) {
				throw new JxtendReflectionException("Supplied type %s for field %s of class %s is not compatible with "
						+ "actual field type: %s",
						fieldType.getName(), field.getName(), field.getDeclaringClass(), field.getType().getName());
			}

			field.setAccessible(true);
		} catch (SecurityException e) {
			throw new JxtendReflectionException(e);
		}
	}
}
